using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using AntEater.ComponentModel;
using AntEater.ComponentModel.BuildElements;
using AntEater.Runtime;
using AntEater.Runtime.Services;
using AntEater.Runtime.Services.Transformation;
using AntEater.Runtime.UI;


namespace AntEater.Integration
{
    /// <summary>
    /// The solution manager holds the effective Visual Studio Solution and the 
    /// solution of the AntEater Runtime in sync. It provides a collection of build
    /// files to the solution of the runtime and removes non existent build files
    /// from the project of the runtime.
    /// </summary>
    internal class SolutionManager : IDisposable
    {
        private IServiceProvider _serviceProvider;
        private IAntEaterRuntime _runtime;

        private uint _solutionEventCookie;
        private uint _documentEventCookie;

        private static Guid hierarchyGuid = typeof(IVsHierarchy).GUID;
        private static SolutionManager _instance;

        /// <summary>
        /// Initializes the specified service provider.
        /// </summary>
        /// <param name="serviceProvider">The service provider.</param>
        /// <param name="runtime">The runtime.</param>
        public static void Initialize(IServiceProvider serviceProvider, IAntEaterRuntime runtime) {
            if (SolutionManager._instance == null) {
                SolutionManager._instance = new SolutionManager(serviceProvider, runtime);
            }
        }

        /// <summary>
        /// Gets the instance.
        /// </summary>
        /// <returns></returns>
        public static SolutionManager GetInstance() {
            return SolutionManager._instance;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:SolutionManager"/> class.
        /// </summary>
        /// <param name="serviceProvider">The service provider.</param>
        /// <param name="runtime">The runtime.</param>
        private SolutionManager(IServiceProvider serviceProvider, IAntEaterRuntime runtime)
            : base() {
            _serviceProvider = serviceProvider;
            _runtime = runtime;

            // Rgeister the solution manager to the solution events of Visual Studio
            this.RegisterSolutionEvents();
        }

        /// <summary>
        /// Opens the file.
        /// </summary>
        /// <param name="buildFilepath">The build filepath.</param>
        /// <param name="view">The view.</param>
        /// <returns></returns>
        public bool OpenFile(string buildFilepath, BuildFileViewType view) {

            bool result = false;

            // Get Shell Service
            IVsUIShellOpenDocument shell = _serviceProvider.GetService(typeof(SVsUIShellOpenDocument)) as IVsUIShellOpenDocument;
            if (shell != null) {

                // Get absolute path of file
                string[] absolutePath = new string[1] { string.Empty };
                shell.SearchProjectsForRelativePath((uint)__VSRELPATHSEARCHFLAGS.RPS_UseAllSearchStrategies, buildFilepath, absolutePath);

                if (!string.IsNullOrEmpty(absolutePath[0])) {

                    IVsUIHierarchy hierarchy;
                    IVsWindowFrame windowFrame;
                    uint itemId;
                    Guid editorId;
                    Guid viewId;

                    // Get specified Logical View
                    switch (view) {
                        case BuildFileViewType.Xml:
                            editorId = new Guid(Guids.XmlDesignerId);
                            viewId = new Guid(LogicalViewID.Code);
                            break;

                        case BuildFileViewType.Designer:
                        default:
                            editorId = Guids.BuildFileEditorFactoryIdGuid;
                            viewId = new Guid(LogicalViewID.Designer);
                            break;
                    }

                    try {
                        // Check if file is allready open
                        bool isOpen = VsShellUtilities.IsDocumentOpen(_serviceProvider, absolutePath[0], viewId, out hierarchy, out itemId, out windowFrame);
                        if (!isOpen) {
                            // Show file if file is not open
                            VsShellUtilities.OpenDocumentWithSpecificEditor(_serviceProvider, absolutePath[0], editorId, viewId);

                        } else {
                            // Show Window if file is allready open
                            windowFrame.Show();
                        }
                    } catch (InvalidCastException ex) {
                        Utilities.ShowException(_serviceProvider, ex);
                        result = false;
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Refreshes this instance.
        /// </summary>
        public void Refresh() {

            BuildFileCollection buildFiles = new BuildFileCollection();

            // Get the solution service from Visual Studio
            IVsSolution solutionService = (IVsSolution)_serviceProvider.GetService(typeof(SVsSolution));
            if (null != solutionService) {
                // Parse solution hierarchy for build files
                buildFiles.AddRange(this.RefreshProjects(solutionService));
                buildFiles.AddRange(this.RefreshItems((IVsHierarchy)solutionService, VSConstants.VSITEMID_ROOT, true, 0));
            }

            // Transform each build file
            foreach (BuildFile buildFile in buildFiles) {

                // Get transformation service
                IXmlToBuildElementTransformationService service;
                service = _runtime.RuntimeServices.GetService(typeof(IXmlToBuildElementTransformationService)) as IXmlToBuildElementTransformationService;

                // Transform the xml build file to the build element
                using (Stream stream = File.Open(buildFile.BuildFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
                    XmlToBuildElementTransformationResult result = service.Transform(stream, buildFile.BuildFilePath);
                    buildFile.Definition = result.BuildFileDefinition;
                    buildFile.Messages.AddRange(result.Errors);
                }
            }

            Solution solution = _runtime.Solution;
            Project project = _runtime.Solution.Project;

            // Sync build files with AntEater solution and project
            foreach (BuildFile buildFile in buildFiles) {

                if (solution.BuildFiles.Contains(buildFile.BuildFilePath)) {
                    solution.BuildFiles[buildFile.BuildFilePath].Definition = buildFile.Definition;
                } else {
                    solution.BuildFiles.Add(buildFile);
                }
            }

            // Remove non existent build files form AntEater project
            foreach (BuildFile projectBuildFile in project.BuildFiles) {
                if (buildFiles.Contains(projectBuildFile.BuildFilePath) == false) {
                    project.BuildFiles.Remove(projectBuildFile);
                }
            }
        }

        /// <summary>
        /// Refreshes the projects.
        /// </summary>
        /// <param name="solution">The solution.</param>
        /// <returns></returns>
        private BuildFileCollection RefreshProjects(IVsSolution solution) {
            BuildFileCollection buildFiles = new BuildFileCollection();

            // Get count of projects in solution
            uint projectCount;
            solution.GetProjectFilesInSolution((uint)__VSGETPROJFILESFLAGS.GPFF_SKIPUNLOADEDPROJECTS, 0, null, out projectCount);

            // Get filenames of projects in solution
            string[] projects = new string[projectCount];
            solution.GetProjectFilesInSolution((uint)__VSGETPROJFILESFLAGS.GPFF_SKIPUNLOADEDPROJECTS, 0, projects, out projectCount);

            for (int i = 0; i < projects.Length; i++) {

                // Check if element is a build file (.build;.csproj;....)
                if ((projects[i] != null) && (_runtime.Options.BuildFileExtensions.IsBuildFile(projects[i]))) {
                    // Add buildfile
                    buildFiles.Add(new BuildFile(projects[i]));
                }
            }

            return buildFiles;
        }

        /// <summary>
        /// Refreshes the items.
        /// </summary>
        /// <param name="hierarchy">The hierarchy.</param>
        /// <param name="itemId">The item id.</param>
        /// <param name="isSolution">if set to <c>true</c> [is solution].</param>
        /// <param name="level">The level.</param>
        /// <returns></returns>
        private BuildFileCollection RefreshItems(IVsHierarchy hierarchy, uint itemId, bool isSolution, int level) {
            BuildFileCollection buildFiles = new BuildFileCollection();

            IntPtr nestedHierarchyPointer;
            uint nestedHierarchyId;

            // Chekc if hierarchy is nested (all hierarchies, which have child elements are nested)
            int result = hierarchy.GetNestedHierarchy(itemId, ref SolutionManager.hierarchyGuid, out nestedHierarchyPointer, out nestedHierarchyId);
            if ((result == VSConstants.S_OK) && (nestedHierarchyPointer != IntPtr.Zero)) {

                // Get reference to hierarchy object out of the pointer
                IVsHierarchy nestedHierarchy = Marshal.GetObjectForIUnknown(nestedHierarchyPointer) as IVsHierarchy;
                Marshal.Release(nestedHierarchyPointer);

                // Parse items from the nested hierarchy
                if (nestedHierarchy != null) {
                    buildFiles.AddRange(this.RefreshItems(nestedHierarchy, nestedHierarchyId, false, level));
                }

            } else {

                // increase hierarchy level
                level++;

                // Get filename of the current hierarchy element
                string filename;
                hierarchy.GetCanonicalName(itemId, out filename);

                // Check if element is a build file (.build)
                if ((filename != null) && (_runtime.Options.BuildFileExtensions.IsBuildFile(filename))) {
                    // Get name of the build file
                    object name;
                    hierarchy.GetProperty(itemId, (int)__VSHPROPID.VSHPROPID_Name, out name);

                    // Addd build file to the collection
                    buildFiles.Add(new BuildFile(filename));
                }

                // Visual Studio has a bug in the hierarchy implementation. If
                // it is the solution element on the first level pass 
                // "FirstVisibleChild" instead of "FirstChild" to the method
                int childPropertyId = ((isSolution) && (level == 1)) ? (int)__VSHPROPID.VSHPROPID_FirstVisibleChild : (int)__VSHPROPID.VSHPROPID_FirstChild;

                // Get the child of the hierarchy
                object childName;
                if (hierarchy.GetProperty(itemId, childPropertyId, out childName) == VSConstants.S_OK) {
                    uint childItemId = SolutionManager.ConvertItemId(childName);

                    // Check if a child exist
                    while (childItemId != VSConstants.VSITEMID_NIL) {
                        // Parse child items of this hierarchy
                        buildFiles.AddRange(this.RefreshItems(hierarchy, childItemId, false, level));

                        // Visual Studio has a bug in the hierarchy implementation. If
                        // it is the solution element on the first level pass 
                        // "NextVisibleSibling" instead of "NextSibling" to the method
                        childPropertyId = ((isSolution) && (level == 1)) ? (int)__VSHPROPID.VSHPROPID_NextVisibleSibling : (int)__VSHPROPID.VSHPROPID_NextSibling;

                        // Get next child element of the hierarchy
                        if (hierarchy.GetProperty(childItemId, childPropertyId, out childName) == VSConstants.S_OK) {
                            childItemId = SolutionManager.ConvertItemId(childName);
                        } else {
                            break;
                        }
                    }
                }
            }

            return buildFiles;
        }

        /// <summary>
        /// Registers the solution events.
        /// </summary>
        private void RegisterSolutionEvents() {

            // Get the solution service of Visual Studio and register for the solution events
            IVsSolution solution = (IVsSolution)_serviceProvider.GetService(typeof(SVsSolution));
            if (solution != null) {
                SolutionListener solutionListener = new SolutionListener();
                solutionListener.SolutionOpened += new EventHandler<EventArgs>(SolutionOpened);
                solutionListener.SolutionClosed += new EventHandler<EventArgs>(SolutionClosed);
                solutionListener.ProjectOpened += new EventHandler<EventArgs>(SolutionChanged);
                solutionListener.ProjectClosed += new EventHandler<EventArgs>(SolutionChanged);

                // Register for the events and store the cookie for unregistering from the events
                solution.AdviseSolutionEvents(solutionListener, out _solutionEventCookie);
            }
        }

        /// <summary>
        /// Unregisters the solution events.
        /// </summary>
        private void UnregisterSolutionEvents() {

            // Get the solution service of Visual Studio and unregister for the solution events
            IVsSolution solution = (IVsSolution)_serviceProvider.GetService(typeof(SVsSolution));
            if (solution != null) {
                solution.UnadviseSolutionEvents(_solutionEventCookie);
            }

        }

        /// <summary>
        /// Registers the project events.
        /// </summary>
        private void RegisterProjectEvents() {

            // Get the document service of Visual Studio and register for the document events
            IVsTrackProjectDocuments2 document = (IVsTrackProjectDocuments2)_serviceProvider.GetService(typeof(SVsTrackProjectDocuments));
            if (document != null) {
                ProjectListener documentListener = new ProjectListener();
                documentListener.ItemAdded += new EventHandler<EventArgs>(SolutionChanged);
                documentListener.ItemDeleted += new EventHandler<EventArgs>(SolutionChanged);
                documentListener.ItemRenamed += new EventHandler<EventArgs>(SolutionChanged);

                // Register for the events and store the cookie for unregistering from the events
                document.AdviseTrackProjectDocumentsEvents(documentListener, out _documentEventCookie);
            }
        }

        /// <summary>
        /// Unregisters the project events.
        /// </summary>
        private void UnregisterProjectEvents() {

            // Get the document service of Visual Studio and unregister for the document events
            IVsTrackProjectDocuments2 document = (IVsTrackProjectDocuments2)_serviceProvider.GetService(typeof(SVsTrackProjectDocuments));
            if (document != null) {
                document.UnadviseTrackProjectDocumentsEvents(_documentEventCookie);
            }
        }

        /// <summary>
        /// Called when [solution opened].
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
        private void SolutionOpened(object sender, EventArgs e) {
            _runtime.Solution.Open();
            this.RegisterProjectEvents();
        }

        /// <summary>
        /// Called when [solution closed].
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
        private void SolutionClosed(object sender, EventArgs e) {
            this.UnregisterProjectEvents();
            _runtime.Solution.Close();
        }

        /// <summary>
        /// Called when [solution changed].
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
        private void SolutionChanged(object sender, EventArgs e) {
            this.Refresh();
        }

        /// <summary>
        /// Converts the item id.
        /// </summary>
        /// <param name="itemId">The item id.</param>
        /// <returns></returns>
        private static uint ConvertItemId(object itemId) {

            uint result = VSConstants.VSITEMID_NIL;

            if (itemId is int) {
                result = (uint)(int)itemId;
            } else if (itemId is uint) {
                result = (uint)itemId;
            } else if (itemId is short) {
                result = (uint)(short)itemId;
            } else if (itemId is ushort) {
                result = (uint)(ushort)itemId;
            } else if (itemId is long) {
                result = (uint)(long)itemId;
            }

            return result;
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose() {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Disposes the specified disposing.
        /// </summary>
        /// <param name="disposing">if set to <c>true</c> [disposing].</param>
        protected void Dispose(bool disposing) {

            if (disposing) {
                // Unregister form the events
                this.UnregisterProjectEvents();
                this.UnregisterSolutionEvents();
            }
        }
    }
}
